Inside Macintosh: Sound

| Previous | Chapter contents | Chapter top | Section top | Next |

Using Callback Procedures

This section shows how you can use callback procedures to play one sound asynchronously at a given time. "Managing Multiple Sound Channels" expands the techniques in this section to show how you can play several asynchronous sounds simultaneously.

The SndNewChannel function allows you to associate a callback procedure with a sound channel. For example, the following code opens a new sound channel for which memory has already been allocated and associates it with the callback procedure MyCallBack :

myErr := SndNewChannel(gSndChan, sampledSynth, initMono, @MyCallback);

After filling a channel created by SndNewChannel with various commands to create sound, you can then issue a callBackCmd command to the channel. When the Sound Manager encounters a callBackCmd command, it executes your callback procedure. Thus, by placing the callBackCmd command last in a channel, you can ensure that the Sound Manager executes your callback procedure only after it has processed all of the channel's other sound commands.

Be sure to issue callBackCmd commands with the SndDoCommand function and not the SndDoImmediate function. If you issue a callBackCmd command with SndDoImmediate , your callback procedure might be called before other sound commands you have issued finish executing.

A callback procedure has the following syntax:

PROCEDURE MyCallBack (chan: SndChannelPtr; cmd: SndCommand);

Because the callback procedure executes at interrupt time, it cannot access its application global variables unless the application's A5 world is set correctly. (For more information on the A5 world, see the chapter "Memory Management Utilities" in Inside Macintosh: Memory .) When called, the callback procedure is passed two parameters: a pointer to the sound channel that received the callBackCmd command and the sound command that caused the callback procedure to be called. Applications can use param1 or param2 of the sound command as flags to pass information or instructions to the callback procedure. If your callback procedure is to use your application's global data storage, it must first reset A5 to your application's A5 and then restore it on exit. For example, Listing 1-28 illustrates how to set up a callBackCmd command that contains the required A5 information in the param2 field. The MyInstallCallback function defined there must be called at a time when your application's A5 world is known to be valid.

Listing 28 Issuing a callback command

FUNCTION MyInstallCallback (mySndChan: SndChannelPtr): OSErr;
CONST
    kWaitIfFull = TRUE;                     {wait for room in queue}
VAR
    mySndCmd:       SndCommand;             {a sound command}
BEGIN
    WITH mySndCmd DO
    BEGIN
        cmd := callBackCmd;                 {install the callback command}
        param1 := kSoundComplete;           {last command for this channel}
        param2 := SetCurrentA5;             {pass the callback the A5}
    END;
    MyInstallCallback := SndDoCommand(mySndChan, mySndCmd, kWaitIfFull);
END;

In this function, kSoundComplete is an application-defined constant that indicates that the requested sound has finished playing. You could define it like this:

CONST
    kSoundComplete = 1;{sound is done playing}

Because param2 of a sound command is a long integer, Listing 1-28 uses it to pass the application's A5 to the callback procedure. That allows the callback procedure to gain access to the application's A5 world.

You can also pass information to a callback routine in the userInfo field of the sound channel.

The sample callback procedure defined in Listing 1-29 can thus set A5 to access the application's global variables.

Listing 29 -- Defining a callback procedure

PROCEDURE MyCallback (theChan: SndChannelPtr; theCmd: SndCommand);
VAR
    myA5:               LongInt;
BEGIN
    IF theCmd.param1 = kSoundComplete THEN
    BEGIN
        myA5 := SetA5(theCmd.param2);    {set my A5}
        gCallbackPerformed := TRUE;    {set a global flag}
        myA5 := SetA5(myA5);    {restore the original A5}
    END;
END;

WARNING
Callback procedures are called at interrupt time and therefore must not attempt to allocate, move, or dispose of memory, dereference an unlocked handle, or call other routines that do so. Also, assembly-language programmers should note that a callback procedure is a Pascal procedure and must preserve all registers other than A0–A1 and D0–D2.

Callback procedures cannot dispose of channels themselves, because that involves disposing of memory. To circumvent this restriction, the callback procedure in Listing 2-29 simply sets the value of a global flag variable that your application defines. Then, once each time through its main event loop, your application must call a routine that checks to see if the flag is set. If the flag is set, the routine should dispose of the channel, release any other memory allocated specifically for use in the channel, and reset the flag variable. Listing 2-30 defines such a routine. Your application should call it once each time through its main event loop.

Listing 30 -- Checking whether a callback procedure has executed

PROCEDURE MyCheckSndChan; CONST
    kQuietNow = TRUE;    {need to quiet channel?}
VAR
    myErr:    OSErr;
BEGIN
    IF gCallbackPerformed THEN    {check global flag}
    BEGIN    {channel is done}
        gCallbackPerformed := FALSE;    {reset global flag}
        IF gSndChan^.userInfo <> 0 THEN
        BEGIN    {release sound data}
            HUnlock(Handle(gSndChan^.userInfo));
            HPurge(Handle(gSndChan^.userInfo));
        END;
        myErr := MyDisposeSndChannel(gSndChan, kQuietNow);
        gSndChan := NIL;    {set pointer to NIL}
    END;
END;

The MyCheckSndChan procedure defined in Listing 2-30 checks the userInfo field of the sound channel to see if it contains the address of a handle. Thus, if you would like the MyCheckSndChan procedure to release memory associated with a sound handle, you need only put the address of the handle in the userInfo field of the sound channel. (If you do not want the MyCheckSndChan procedure to release memory associated with a handle, then you should set the userInfo field to 0 when you allocate the channel. The MyCreateSndChannel function defined in Listing 2-1 automatically sets this field to 0.) After releasing the memory associated with the sound handle, the MyCheckSndChan procedure calls the MyDisposeSndChannel function (defined in Listing 2-3) to release the memory occupied by both the sound channel and the sound channel record.

To ensure that the MyCheckSndChan procedure defined in Listing 2-30 does not attempt to dispose a channel before you have created one, you should initialize the gCallbackPerformed variable to FALSE. Also, you should initialize the gSndChan variable to NIL, so that other parts of your application can check to see if a sound is playing simply by checking this variable. For example, if your application must play a sound but another sound is currently playing, you might ensure that the application gives priority to the newer sound by stopping the old one. Listing 2-31 defines a procedure that stops the sound that is playing.

Listing 31 -- Stopping a sound that is playing asynchronously

PROCEDURE MyStopPlaying;
BEGIN
    IF gSndChan <> NIL THEN     {is sound really playing?}
    gCallbackPerformed := TRUE; {set global flag}
    MyCheckSndChan;             {call routine to do disposing}
END;

Once you have defined a callback procedure, a routine that installs the callback procedure, a routine that checks the status of the callback procedure, and a routine that can stop sound play, you need only allocate a sound channel, call the SndPlay function, and install your callback procedure to start an asynchronous sound play. Listing 2-32 defines a procedure that starts an asynchronous play.

Listing 32 -- Starting an asynchronous sound play

PROCEDURE MyStartPlaying (mySndID: Integer);
CONST
    kAsync = TRUE;    {play is asynchronous}
VAR
    mySndHandle:    Handle;    {handle to an 'snd ' resource}
    myErr:    OSErr;
BEGIN
    IF gSndChan <> NIL THEN    {check if channel is active}
        MyStopPlaying;
    gSndChan := MyCreateSndChannel(0, 0, @MyCallbackProc, stdQLength);
    mySndHandle := GetResource('snd ', mySndID);
    IF (mySndHandle <> NIL) AND (gSndChan <> NIL) THEN
    BEGIN    {start sound playing}
        DetachResource(mySndHandle);    {detach resource from file}
            {remember to release sound handle}
        gSndChan^.userInfo := LongInt(mySndHandle);
        HLock(mySndHandle);    {lock the resource data}
        myErr := SndPlay(gSndChan, mySndHandle, kAsync);
        IF myErr = noErr THEN
            myErr := MyInstallCallback(gSndChan);
        IF myErr <> noErr THEN
            DoError(myErr);
    END;
END;

The MyStartPlaying procedure uses the MyCreateSndChannel function defined in Listing 2-1 to create a sound channel, requesting that the function allocate a standard-sized sound channel command queue. By using such a queue, you can be sure that your application can play any sound resource that contains up to 127 sound commands. If you are sure that your application will play only sampled-sound resources created by the Sound Input Manager, you should request a queue of only two sound commands, thereby leaving enough room for just the bufferCmd command contained within the sound resource and the callBackCmd command that your application issues.

Before playing the sound, the MyStartPlaying procedure defined in Listing 2-32 detaches the sound resource from its resource file after loading it. This is important if the resource file could close while the sound is still playing, or if your application might create another sound channel to play the same sound resource while the sound is still playing.


© 1998 Apple Computer, Inc.

| Previous | Chapter contents | Chapter top | Section top | Next |